package com.polaris.live.common.network.interceptor

import androidx.core.text.isDigitsOnly
import com.polaris.live.common.network.AppException
import com.polaris.live.common.network.EncryptHelper
import com.polaris.live.common.util.JsonUtils
import com.polaris.live.common.util.LogExt
import okhttp3.Connection
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Interceptor.Chain
import okhttp3.Response
import okio.Buffer
import okio.GzipSource
import java.io.EOFException
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit

/**
 * 日志拦截器
 *
 * @author Created by lucas on 2022/1/22 17:32
 * @since 1.0.0
 */
class HttpLoggingInterceptor(
    private val printLogging: Boolean
) : Interceptor {

    private val SUFFIX_MAP = mapOf(
        0 to "Request Headers",
        1 to "Request Params",
        2 to "Request Body",
        3 to "Response Headers",
        4 to "Response Body"
    )

    override fun intercept(chain: Chain): Response {
        val request = chain.request()
        if (!printLogging) {
            return chain.proceed(request)
        }

        val method = request.method
        val httpUrl = request.url

        val url = resolveUrl(httpUrl, chain.connection())

        val requestHeader = LinkedHashMap<String, String>()
        val requestBody = request.body

        if (requestBody != null) {
            // Request body headers are only present when installed as a network interceptor. Force
            // them to be included (when available) so there values are known.
            if (requestBody.contentType() != null) {
                requestHeader["Content-Type"] = requestBody.contentType().toString()
            }
            if (requestBody.contentLength() != -1L) {
                requestHeader["Content-Length"] = requestBody.contentLength().toString() + "-byte"
            }
        }

        request.headers.populateHeader(requestHeader)

        val requestParams = resolveRequestParams(httpUrl)
        val requestHeaderJson = mapToJson(requestHeader)
        val requestBodyParams = mapToJson(requestParams)
        var requestBodyJson: String? = null

        if (requestBody != null) {
            val buffer = Buffer()
            requestBody.writeTo(buffer)
            if (isPlaintext(buffer)) {
                requestBodyJson = buffer.readString(StandardCharsets.UTF_8)
            }
            val contentType = requestHeader["Content-Type"]
            if ("application/x-www-form-urlencoded" == contentType
                && !requestBodyJson.isNullOrEmpty()
            ) {
                requestBodyJson = resolveFromBody(requestBodyJson)
            }

            if (requestBodyJson != null && method == "POST" && contentType?.startsWith("application/json") == true) {
                requestBodyJson = EncryptHelper.decryptParams(requestBodyJson)
            }
        }

        //request结束
        var responseHeaderJson: String? = null
        var responseBodyJson: String? = null

        val startNs = System.nanoTime()
        var tookMs = -1L

        return try {
            chain.proceed(request).apply {
                tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)

                val responseHeaders = HashMap<String, String>()
                val responseBody = body

                val contentLength = responseBody?.contentLength() ?: 0

                if (responseBody != null) {
                    if (responseBody.contentType() != null) {
                        responseHeaders["Content-Type"] = responseBody.contentType().toString()
                    }
                    responseHeaders["Content-Length"] = "$contentLength-byte"
                }

                headers.populateHeader(responseHeaders)

                if (responseBody != null) {
                    val source = responseBody.source()
                    source.request(Long.MAX_VALUE) // Buffer the entire body.
                    val buffer = source.buffer
                    if (!isPlaintext(buffer)) {
                        return this
                    }

                    var gzippedLength: Long? = null
                    if ("gzip".equals(responseHeaders["Content-Encoding"], ignoreCase = true)) {
                        gzippedLength = buffer.size
                        GzipSource(buffer.clone()).use {
                            Buffer().use { newBuffer ->
                                newBuffer.writeAll(it)
                            }
                        }
                    }

                    if (gzippedLength != null) {
                        responseHeaders["Content-Length"] =
                            buffer.size.toString() + "-byte, " + gzippedLength + "-gzipped-byte body"
                    } else {
                        responseHeaders["Content-Length"] = buffer.size.toString() + "-byte"
                    }

                    responseHeaderJson = mapToJson(responseHeaders)

                    if (contentLength != 0L) {
                        responseBodyJson = buffer.clone().readString(StandardCharsets.UTF_8)
                    }
                }
            }
        } catch (t: Throwable) {
            if (t is AppException) {
                throw t
            }

            tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)

            responseHeaderJson = "{\"response\":\"failure\"}"
            responseBodyJson = "{\"message\":\"${getThrowableMessage(t)}\"}"

            throw t
        } finally {
            if (tookMs != -1L) {
                val formatUrl = "$method：$url ($tookMs ms)"



                LogExt.logJson(
                    "--net",
                    formatUrl,
                    requestHeaderJson,
                    requestBodyParams,
                    requestBodyJson,
                    responseHeaderJson,
                    responseBodyJson,
                    suffix = SUFFIX_MAP
                )
            }
        }
    }

    private fun Headers.populateHeader(requestHeader: MutableMap<String, String>) {
        for (i in 0 until size) {
            val name = name(i)
            // Skip headers from the request body as they are explicitly logged above.
            if (!"Content-Type".equals(name, ignoreCase = true)
                && !"Content-Length".equals(name, ignoreCase = true)
            ) {
                requestHeader[name] = value(i)
            }
        }
    }

    private fun resolveFromBody(requestBodyJson: String): String {


        val params = requestBodyJson.split("&".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
        if (params.isEmpty()) {
            return requestBodyJson
        }
        val builder = StringBuilder("{")

        val count = params.size
        for (i in 0 until count) {
            val param = params[i]

            val split = param.split("=")
            if (split.size != 2) {
                continue
            }

            val key = URLDecoder.decode(split[0], "UTF-8")
            val value = URLDecoder.decode(split[1], "UTF-8")

            builder.append("\"")
                .append(key)
                .append("\":")

            if (value.isDigitsOnly()) {
                builder.append(value)
            } else {
                builder.append("\"")
                    .append(value)
                    .append("\"")
            }

            if (i < count - 1) {
                builder.append(",")
            }
        }

        builder.append("}")

        return builder.toString()
    }

    private fun getThrowableMessage(t: Throwable): String? {
        var lastThrowable: Throwable? = t
        var i = 0
        while (lastThrowable != null && i++ < 5) {
            val message = lastThrowable.message
            if (!message.isNullOrEmpty()) {
                return message
            }

            lastThrowable = lastThrowable.cause
        }

        return null
    }

    private fun resolveUrl(httpUrl: HttpUrl, connection: Connection?): String {
        val protocol = if (connection != null) {
            " " + connection.protocol()
        } else {
            ""
        }

        val url = httpUrl.toString()
        val questionIndex = url.indexOf('?')
        val newUrl = if (questionIndex != -1) {
            url.substring(0, questionIndex)
        } else {
            url
        }
        return newUrl + protocol
    }

    private fun resolveRequestParams(httpUrl: HttpUrl): Map<String, String> {
        val params = HashMap<String, String>()
        for (i in 0 until httpUrl.querySize) {
            val value = httpUrl.queryParameterValue(i)
            if (value.isNullOrEmpty()) {
                continue
            }

            params[httpUrl.queryParameterName(i)] = value
        }
        return params
    }

    private fun mapToJson(param: Map<String, String>): String? {
        return if (param.isEmpty()) {
            null
        } else {
            JsonUtils.toJson(param)
        }
    }

    /**
     * Returns true if the body in question probably contains human readable text. Uses a small sample
     * of code points to detect unicode control characters commonly used in binary file signatures.
     */
    private fun isPlaintext(buffer: Buffer): Boolean {
        return try {
            val prefix = Buffer()
            val byteCount = if (buffer.size < 64) {
                buffer.size
            } else {
                64
            }
            buffer.copyTo(prefix, 0, byteCount)
            for (i in 0..15) {
                if (prefix.exhausted()) {
                    break
                }
                val codePoint = prefix.readUtf8CodePoint()
                if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
                    return false
                }
            }
            true
        } catch (e: EOFException) {
            false // Truncated UTF-8 sequence.
        }
    }
}